﻿using System;
using System.Linq.Expressions;
using System.Reflection;
using Moki.Core;
using Moki.Extensions;
using Moki.State;

namespace Moki
{
    public enum CallConstraints
    {
        Loose,
        Strict
    }

    /// <summary>
    /// Mocking central point.
    /// </summary>
    public static class Mock
    {
        private static readonly MockRepository _mockRepository = MockRepository.Instance;
        private static StubPerformer _stubPerformer = new StubPerformer(MockRepository.Instance, StateController.Instance);
        private static Verifier _verifier = new Verifier(MockRepository.Instance, StateController.Instance);
        private static EventRaiser _raiser = new EventRaiser(StateController.Instance);

        internal static MockRepository Repository
        {
            get { return _mockRepository; }
        }

        #region Create
        /// <summary>
        /// Creates mock of given type.
        /// </summary>
        /// <typeparam name="TType">Mock type</typeparam>
        /// <returns>Mock object</returns>
        public static TType Create<TType>() where TType : class
        {
            return (TType)Create(typeof(TType), CallConstraints.Loose);
        }

        /// <summary>
        /// Creates mock of given type with additional interfaces.
        /// </summary>
        /// <typeparam name="TType">Mock type</typeparam>
        /// <typeparam name="TInterface1">Additional interface</typeparam>
        /// <returns>Mock object</returns>
        public static TType Create<TType, TInterface1>() where TType : class
        {
            return (TType)Create(typeof(TType), new Type[] { typeof(TInterface1) }, CallConstraints.Loose);
        }

        /// <summary>
        /// Creates mock of given type with additional interfaces.
        /// </summary>
        /// <typeparam name="TType">Mock type</typeparam>
        /// <typeparam name="TInterface1">Additional interface</typeparam>
        /// <typeparam name="TInterface2">Additional interface</typeparam>
        /// <returns>Mock object</returns>
        public static TType Create<TType, TInterface1, TInterface2>() where TType : class
        {
            return (TType)Create(typeof(TType), new Type[] { typeof(TInterface1), typeof(TInterface2) }, CallConstraints.Loose);
        }

        /// <summary>
        /// Creates mock of given type.
        /// </summary>
        /// <typeparam name="TType">Mock type</typeparam>
        /// <param name="constructorArguments">The constructor arguments.</param>
        /// <returns>Mock object</returns>
        public static TType Create<TType>(params object[] constructorArguments) where TType : class
        {
            return (TType)Create(typeof(TType), CallConstraints.Loose, constructorArguments);
        }

        /// <summary>
        /// Creates mock of given type with additional interfaces.
        /// </summary>
        /// <typeparam name="TType">Mock type</typeparam>
        /// <typeparam name="TInterface1">Additional interface</typeparam>
        /// <param name="constructorArguments">The constructor arguments.</param>
        /// <returns>Mock object</returns>
        public static TType Create<TType, TInterface1>(params object[] constructorArguments) where TType : class
        {
            return (TType)Create(typeof(TType), new Type[] { typeof(TInterface1) }, CallConstraints.Loose, constructorArguments);
        }

        /// <summary>
        /// Creates mock of given type with additional interfaces.
        /// </summary>
        /// <typeparam name="TType">Mock type</typeparam>
        /// <typeparam name="TInterface1">Additional interface</typeparam>
        /// <typeparam name="TInterface2">Additional interface</typeparam>
        /// <param name="constructorArguments">The constructor arguments.</param>
        /// <returns>Mock object</returns>
        public static TType Create<TType, TInterface1, TInterface2>(params object[] constructorArguments) where TType : class
        {
            return (TType)Create(typeof(TType), new Type[] { typeof(TInterface1), typeof(TInterface2) }, CallConstraints.Loose, constructorArguments);
        }

        /// <summary>
        /// Creates mock of given type with additional interfaces.
        /// </summary>
        /// <typeparam name="TType">Mock type</typeparam>
        /// <param name="constraints">The mock constraints.</param>
        /// <returns></returns>
        public static TType Create<TType>(CallConstraints constraints) where TType : class
        {
            return (TType)Create(typeof(TType), constraints);
        }

        /// <summary>
        /// Creates mock of given type with additional interfaces.
        /// </summary>
        /// <typeparam name="TType">Mock type</typeparam>
        /// <param name="constraints">The mock constraints.</param>
        /// <param name="constructorArguments">The constructor arguments.</param>
        /// <returns>Mock object</returns>
        public static TType Create<TType>(CallConstraints constraints, params object[] constructorArguments) where TType : class
        {
            return (TType)Create(typeof(TType), constraints, constructorArguments);
        }

        internal static object Create(Type type, CallConstraints constraints, params object[] constructorArguments)
        {
            return _mockRepository.CreateMock(type, null, constraints, constructorArguments);
        }

        internal static object Create(Type type, Type[] additionalInterfaces, CallConstraints constraints, params object[] constructorArguments)
        {
            return _mockRepository.CreateMock(type, additionalInterfaces, constraints, constructorArguments);
        }

        internal static object Create(Type type, CallConstraints constraints)
        {
            return _mockRepository.CreateMock(type, null, constraints, null);
        }

        internal static object Create(Type type, Type[] additionalInterfaces, CallConstraints constraints)
        {
            return _mockRepository.CreateMock(type, additionalInterfaces, constraints, null);
        }
        #endregion

        #region When

        /// <summary>
        /// Sets behavior for method call.
        /// </summary>
        /// <code>Mock.WhenCalled(() => mock.DoSomething("param"));</code>
        /// <param name="action">Expression method call. For instance <c> () => mock.Foo() </c></param>
        /// <returns></returns>
        public static ICall WhenCalled(Expression<Action> action)
        {
            _stubPerformer.BeginStubbing();

            var method = action.GetMethod();
            method.Validate();

            var arguments = action.GetMethodArgsWithMatchers();
            var proxy = action.GetObjectByInvoke();

            var call = _stubPerformer.GetStub(proxy, method, arguments);

            if (call == null)
            {
                call = new StubContext(method, arguments);
                _stubPerformer.AddStub(proxy, call);
            }

            _stubPerformer.EndStubbing();

            return new Call(call, _stubPerformer);
        }

        /// <summary>
        /// Sets behavior for get property call. 
        /// </summary>
        /// <code>  Mock.WhenGet(() => mock.Property);</code>
        /// <param name="action">Expression property call. For instance <c> () => mock.Bar </c></param>
        /// <returns></returns>
        public static ICall WhenGet(Expression<Func<object>> action)
        {
            _stubPerformer.BeginStubbing();

            var method = action.GetPropertyMethod();
            method.Validate();

            var proxy = action.GetObjectByInvoke();
            var call = _stubPerformer.GetStub(proxy, method);

            if (call == null)
            {
                call = new StubContext(method);
                _stubPerformer.AddStub(proxy, call);
            }

            _stubPerformer.EndStubbing();

            return new Call(call, _stubPerformer);
        }

        /// <summary>
        /// Sets behavior for property set. 
        /// </summary>
        /// <code>  Mock.WhenSet(() => mock.Property); </code>
        /// <param name="action">Expression property call. For instance <c> () => mock.Bar </c></param>
        /// <returns></returns>
        public static ISetCall WhenSet(Expression<Func<object>> action)
        {
            return WhenSet(action, Arg.Any<object>());
        }

        /// <summary>
        /// Sets behavior for property set with given value. 
        /// </summary>
        /// <code>  Mock.WhenSet(() => mock.Property) </code>
        /// <param name="action">Expression property call. For instance <c> () => mock.Bar </c></param>
        /// <param name="value">Property set value.</param>
        /// <returns></returns>
        public static ISetCall WhenSet(Expression<Func<object>> action, object value)
        {
            _stubPerformer.BeginStubbing();

            var matchersArray = MatchersBuilder.GetMatchersArray();
            MatchersBuilder.Reset();

            if (matchersArray.Length == 0)
                matchersArray = new Matcher[] { null };

            var obj = action.GetObjectByInvoke();
            MethodInfo setMethod = action.GetPropertyMethod().ToProperty(obj.GetType()).GetSetMethod();

            setMethod.Validate();

            var call = new StubContext(setMethod, new Arguments(new object[] { value }, matchersArray));

            _stubPerformer.AddStub(obj, call);
            _stubPerformer.EndStubbing();
            return new Call(call, _stubPerformer);
        }
        #endregion

        #region Verify

        /// <summary>
        /// Verifies method was called with any arguments.
        /// </summary>
        /// <code>  Mock.VerifyWithAnyArg(() => mock.DoSomething()); </code>
        /// <param name="action">Expression method call. For instance <c> () => mock.Foo() </c></param>
        public static void VerifyCalledWithAnyArgs(Expression<Action> action)
        {
            VerifyCalledWithAnyArgs(action, Times.AtLeast(1), String.Empty);
        }

        /// <summary>
        /// Verifies method was called given number of times with any arguments.
        /// </summary>
        /// <code>  Mock.VerifyWithAnyArg(() => mock.DoSomething(), Times.AtMost(5)); </code>
        /// <param name="action">Expression method call. For instance <c> () => mock.Foo() </c></param>
        /// <param name="times">Number of calls.</param>
        public static void VerifyCalledWithAnyArgs(Expression<Action> action, Times times)
        {
            VerifyCalledWithAnyArgs(action, times, String.Empty);
        }

        /// <summary>
        /// Verifies method was called given number of times with any arguments.
        /// </summary>
        /// <code>  Mock.VerifyWithAnyArg(() => mock.DoSomething(), "custom message"); </code>
        /// <param name="action">Expression method call. For instance <c> () => mock.Foo() </c></param>
        /// <param name="message">Custom error message.</param>
        public static void VerifyCalledWithAnyArgs(Expression<Action> action, string message)
        {
            VerifyCalledWithAnyArgs(action, Times.AtLeast(1), message);
        }

        /// <summary>
        /// Verifies method was called given number of times with any arguments.
        /// </summary>
        /// <code>  Mock.VerifyWithAnyArg(() => mock.DoSomething(), Times.AtMost(5), "custom message"); </code>
        /// <param name="action">Expression method call. For instance <c> () => mock.Foo() </c></param>
        /// <param name="times">Number of calls.</param>
        /// <param name="message">Custom error message.</param>
        public static void VerifyCalledWithAnyArgs(Expression<Action> action, Times times, string message)
        {
            _verifier.BeginVeryfing();
            var method = action.GetMethod();
            method.Validate();

            _verifier.VerifyWasCalledWithAnyArg(action.GetObjectByInvoke(), method, times.ConditionChecker, message);
            _verifier.EndVeryfing();
        }

        /// <summary>
        /// Verifies method was called with given arguments.
        /// </summary>
        /// <code>  Mock.VerifyCalled(() => mockObject.DoSomething()); </code>
        /// <param name="action">Expression method call. For instance <c> () => mock.Foo() </c></param>
        public static void VerifyCalled(Expression<Action> action)
        {
            VerifyCalled(action, Times.AtLeast(1));
        }

        /// <summary>
        /// Verifies method was called given number of times with given arguments.
        /// </summary>
        /// <code>  Mock.VerifyCalled(() => mock.DoSomething(), Times.AtMost(5)); </code>
        /// <param name="action">Expression method call. For instance <c> () => mock.Foo() </c></param>
        /// <param name="times">Number of calls.</param>
        public static void VerifyCalled(Expression<Action> action, Times times)
        {
            _verifier.BeginVeryfing();
            var method = action.GetMethod();
            method.Validate();
            _verifier.VerifyWasCalled(action.GetObjectByInvoke(), action.GetMethod(), action.GetMethodArgsWithMatchers(), times.ConditionChecker);
            _verifier.EndVeryfing();
        }

        /// <summary>
        /// Verifies property was get.
        /// </summary>
        /// <code>  Mock.VerifyGet(() => mock.Property); </code>
        /// <param name="action">Expression property call. For instance <c> () => mock.Bar </c></param>
        public static void VerifyGet(Expression<Func<object>> action)
        {
            VerifyGet(action, Times.AtLeast(1));
        }

        /// <summary>
        /// Verifies property was get given number of times
        /// </summary>
        /// <code>  Mock.VerifyGet(() => mock.Property, Times.AtMost(5)); </code>
        /// <param name="action">Expression property call. For instance <c> () => mock.Bar </c></param>
        /// <param name="times">Number of calls.</param>
        public static void VerifyGet(Expression<Func<object>> action, Times times)
        {
            _verifier.BeginVeryfing();
            var method = action.GetPropertyMethod();
            method.Validate();
            _verifier.VerifyCalledWithAnyArg(action.GetObjectByInvoke(), method, times.ConditionChecker);
            _verifier.EndVeryfing();
        }

        /// <summary>
        /// Verifies property was not get.
        /// </summary>
        /// <code>  Mock.VerifyNotGet(() => mock.Property); </code>
        /// <param name="action">Expression property call. For instance <c> () => mock.Bar </c></param>
        public static void VerifyNotGet(Expression<Func<object>> action)
        {
            _verifier.BeginVeryfing();
            var method = action.GetPropertyMethod();
            method.Validate();
            _verifier.VerifyNotCalled(action.GetObjectByInvoke(), method);
            _verifier.EndVeryfing();
        }

        /// <summary>
        /// Verifies method was not called.
        /// </summary>
        /// <code>  Mock.VerifyNotCalled(() => mock.DoSomething()); </code>
        /// <param name="action">Expression method call. For instance <c> () => mock.Foo() </c></param>
        public static void VerifyNotCalled(Expression<Action> action)
        {
            _verifier.BeginVeryfing();
            var method = action.GetMethod();
            method.Validate();
            _verifier.VerifyNotCalled(action.GetObjectByInvoke(), method);
            _verifier.EndVeryfing();
        }

        /// <summary>
        /// Verifies method was set by given value
        /// </summary>
        /// <code>  Mock.VerifySet(() => mock.Property, value); </code>
        /// <param name="action">Expression property call. For instance <c> () => mock.Bar </c></param>
        public static void VerifySet(Expression<Func<object>> action, object value)
        {
            _verifier.BeginVeryfing();
            var obj = action.GetObjectByInvoke();
            MethodInfo setMethod = action.GetPropertyMethod().ToProperty(obj.GetType()).GetSetMethod();
            setMethod.Validate();
            _verifier.VerifyWasCalled(obj, setMethod, new Arguments(new object[] { value }), Times.AtLeast(1).ConditionChecker);
            _verifier.EndVeryfing();
        }

        /// <summary>
        /// Verifies method was not set.
        /// </summary>
        /// <code>  Mock.VerifyNotSet(() => mock.Property, value); </code>
        /// <param name="action">Expression property call. For instance <c> () => mock.Bar </c></param>
        public static void VerifyNotSet(Expression<Func<object>> action)
        {
            _verifier.BeginVeryfing();
            var obj = action.GetObjectByInvoke();
            MethodInfo setMethod = action.GetPropertyMethod().ToProperty(obj.GetType()).GetSetMethod();
            setMethod.Validate();
            _verifier.VerifyNotCalled(obj, setMethod);
            _verifier.EndVeryfing();
        }

        /*
        public static void VerifyRaise(Action action)
        {
            // TODO
            _verifier.BeginVeryfing();
            // var obj = action.GetObjectByInvoke();
            // MethodInfo setMethod = action.GetPropertyMethod().ToProperty(obj.GetType()).GetSetMethod();
            // _verifier.VerifyWasNotCalled(obj, setMethod);
            _verifier.EndVeryfing();
        }


        public static void VerifyRaise(Action action, object args)
        {
            _verifier.BeginVeryfing();
            // var obj = action.GetObjectByInvoke();
            // MethodInfo setMethod = action.GetPropertyMethod().ToProperty(obj.GetType()).GetSetMethod();
            // _verifier.VerifyWasNotCalled(obj, setMethod);
            _verifier.EndVeryfing();
        } 
         */
        #endregion

        #region Raise
        public static void RaiseEvent(Action action, EventArgs eventArgs)
        {
            _raiser.Raise(action, new object[] { _raiser, eventArgs });
        }

        public static void RaiseEvent(Action action, params object[] eventArgs)
        {
            _raiser.Raise(action, eventArgs);
        }
        #endregion

        /// <summary>
        /// Clean-ups mock repository. Removes all created mocks and calls.
        /// </summary>
        public static void CleanUp()
        {
            _mockRepository.Cleanup();
        }
    }
}
